Skip to content

CSV import: explicit column-to-property mapping - #26

Draft
amirrza777 wants to merge 46 commits into
mde-optimiser:mainfrom
amirrza777:amir/csv-explicit-mapping
Draft

CSV import: explicit column-to-property mapping#26
amirrza777 wants to merge 46 commits into
mde-optimiser:mainfrom
amirrza777:amir/csv-explicit-mapping

Conversation

@amirrza777

@amirrza777 amirrza777 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Relates to #22, stacked on #23 (needs the CSV import contribution plugin merged first).

What this adds

An optional explicit mapping block after the CSV file path:

import CSV {
    Person from "staff.csv" {
        "full_name" = name
        "years" = age
    }
}
  • When a mapping is given, only the listed columns are imported. This is also how a column is intentionally skipped, no separate syntax needed.
  • When no mapping is given, columns are matched to properties by name, same as before.
  • An unmapped CSV column or an unknown property in the mapping produces a warning rather than failing the import.
  • The diagram honours the mapping too, so a file whose column names do not match the metamodel still shows its values rather than appearing empty.

Syntax note

An earlier version of this PR used square brackets, and this description used to claim that reusing curly braces caused a parsing ambiguity. That was wrong, and @szschaler was right to push back on it.

I tested it rather than reasoning about it: with the mapping block using plain nested braces, a file mixing a mapped and an unmapped import parses with no errors, no ambiguity warnings from Chevrotain, and imports correctly. The mapping block can only start with {, while the enclosing import list either continues with an ID or ends with }, so a single token of lookahead is enough to tell them apart.

The syntax is now plain nested braces, consistent with the nested blocks in the other DSLs, which is what @nk-coding asked for originally.

Draft until #23 is merged, since this branches from it.

amirrza777 added 30 commits July 9, 2026 09:31
The POST /api/projects/{id}/csv-import endpoint materialized a CSV into
a standalone .metamodel/.model file pair using metamodel inference.
That workflow is superseded by the import CSV {} syntax, which reads
CSV data directly at render/execution time against an existing,
manually authored metamodel. Removes CsvImportService, CsvImportRoutes,
the now-unused CsvModelInference engine and its tests, the backend's
:metamodel dependency (no longer needed), and the unused
CSV_IMPORT_FAILED error code.
The import CSV {} syntax was hardcoded directly into the base model
grammar. Model now exposes a generic extension point (imports:
[BaseModelImport], mirroring Config's sections: [BaseConfigSection]),
and CSV is implemented as a real contribution plugin using the same
SerializedGrammar-based merging approach as config/config-mdeo:

- ModelContributionPlugin now carries a serialized grammar and import
  metadata instead of raw parser rule objects, so it can cross service
  boundaries as plain JSON
- resolveModelPlugins deserializes and wraps each contributed import in
  a BaseModelImport-extending rule, and createModelRule builds the root
  Model rule from whatever imports are actually resolved
- language-model-csv now builds a real contribution plugin (external
  references for Class/ID/STRING/NEWLINE so its serialized grammar
  doesn't duplicate definitions already provided by the host)
- New service-model-csv microservice mirrors service-config-mdeo:
  registers the CSV contribution with the model language and hosts its
  standalone generated language service
- Deleted the dead mutable-registry scaffolding (registerModelContributionPlugin)
  that caused the original ESM dual-instance bug, and the orphaned files
  left over from the previous abandoned attempt
- modelScopeProvider's class-reference resolution is now plugin-agnostic
  (dispatches on the "class" property generically instead of hardcoding
  CsvClassImport by type)
- Fixed a pre-existing bug in modelDataHandler where the CSV file path
  was passed as a full URI to an API expecting a plain relative path

Verified end to end: grammar parses with no errors, live diagram
rendering from CSV still works, and the model-data execution pipeline
correctly resolves CSV-backed instances.
npm ci in CI failed because these two new packages, added as part of
the contribution-plugin refactor, were never registered in the
committed lock file.
@szschaler

Copy link
Copy Markdown
Member

No problem with the square brackets, but should this be noted as an issue with the main parsing algorithm, @nk-coding ? I must admit I don't fully understand what the issue is there, perhaps @amirrza777 you can explain in a bit more detail?

- nginx: /plugin/csv/ location was missing the proxy_http_version,
  forwarded headers, and cross-origin headers every other plugin
  location has, breaking ESM module loading and cross-origin isolation
  for the CSV plugin specifically.
- service-model: register the generated model language (.m_gen) that
  was dropped when this service was rewritten for the CSV contribution
  plugin refactor, even though the platform still produces .m_gen
  files (e.g. optimizer solutions) and language-model still has full
  support for it.
- modelDataHandler: a CSV import present in a .m file was replacing
  all hand-authored objects/links instead of merging with them, so
  mixing manually-authored data with CSV-imported data in the same
  file silently lost the manual entries.
- Moved the quote-aware CSV row/field parser out of
  language-model-csv (used by the backend import) into language-shared
  so language-model's live diagram rendering (modelGModelFactory) can
  use the same correct parser instead of a naive split(",")/split("\n")
  that miscounts rows when a field contains a quoted comma or newline.

Verified live: mixed manual + CSV objects now render together in the
diagram (confirmed with a hand-authored Company instance alongside
CSV-imported Person rows), .m_gen is now registered in the plugin
manifest, and the /plugin/csv/ headers match the other plugin
locations.
@amirrza777

Copy link
Copy Markdown
Contributor Author

Happy to go into more detail. The grammar looks like this (csvImportRules.ts):

CsvImportContentRule: "{" many(or(add("imports", CsvClassImportRule), NEWLINE)) "}"
CsvClassImportRule: class, "from", file, optional("{", many(mapping), "}") <- original, ambiguous version

The outer import block and the per-class mapping list are nested: one entry inside a many(or(...)) loop that keeps matching either another entry or a newline until it hits the closing }, and each entry itself optionally has its own { mappings } suffix. When both levels use the same {/}, the parser can't reliably tell whether a given } closes the entry's own mapping list or the whole outer block, since chevrotain's LL(k) parser commits to an interpretation using fixed lookahead rather than backtracking. In practice this showed up as the parser splitting one entry into two: the mapping list's own { got misread as the start of a brand new CsvClassImport, and the AST had a second, bogus import whose "class" was actually one of the mapping tokens, failing to resolve as a reference.

Switching the mapping list to [ ] instead removes the ambiguity entirely, since chevrotain can now distinguish which closing bracket belongs to which nesting level by token type instead of guessing from context.

@szschaler on whether this is a main-parsing-algorithm issue: not really, in the sense that this isn't a Langium/chevrotain bug. It's a known, general limitation of LL(k) parsers: reusing the same delimiter for two nested optional/repeated structures makes the grammar locally ambiguous, and the standard fix in any LL(k)-based grammar (not specific to this codebase) is exactly what I did here, giving each nesting level its own distinct delimiter. @nk-coding might want to weigh in if there's a broader pattern here worth documenting for future grammar additions, but I don't think it needs a separate issue against the parser itself.

@nk-coding

Copy link
Copy Markdown
Collaborator

I really do not have a clue what is going on here
There are more then enough nested curly bracket things in the various DSLs, there should definitely be a way to do this, just have a look how it is done there
I would like to avoid the square brackets for this part of the grammar

Niklas asked to avoid square brackets and instead follow how other
DSLs in this codebase disambiguate two adjacent brace blocks in one
rule: an explicit keyword between them, not a different bracket type
(e.g. model-transformation's match { pattern } then { block }).

The explicit mapping list now reads:
  Person from "file.csv" with {
      "column" = property
  }

instead of the previous square-bracket form. Verified live: two CSV
imports in the same block, the first with an explicit mapping, parse
correctly now with no ambiguity errors.
@amirrza777

Copy link
Copy Markdown
Contributor Author

You're right, looked at how model-transformation handles this: match { pattern } then { block } uses two adjacent brace blocks disambiguated by the keyword between them ("then"), not by using a different bracket type. Applied the same technique here: the mapping list is now introduced by a "with" keyword instead of square brackets:

Person from "file.csv" with {
"column" = property
}

Verified live: two CSV imports in the same block, the first with an explicit mapping, parse correctly now with no ambiguity errors. Pushed.

@szschaler

Copy link
Copy Markdown
Member

Why are we writing grammars in abstract syntax notation rather than as EBNF-y text files? I find the current code utterly unreadable.

@amirrza777

Copy link
Copy Markdown
Contributor Author

@szschaler this predates my work (it's the pattern Niklas set up for the whole codebase, e.g. the metamodel/model/model-transformation/config grammars all use it), so he can give the definitive answer, but here's my understanding of why.

A normal Langium project defines its grammar in a static .langium EBNF file, compiled once at build time. This codebase's plugins need something a .langium file can't do: contribution plugins (like this CSV one) get loaded and merged into a host language's grammar (the Model language) at runtime, and which contributions are present isn't known until then. GrammarSerializer/GrammarDeserializer (in language-common) exist specifically to take a grammar built via createRule().as(...) and turn it into a plain JSON-like structure that can be shipped over the wire from a plugin's service and merged into the host grammar's rule set at runtime, something you can't do with a .langium file since Langium compiles that ahead of time into a fixed parser.

So the abstract syntax builder isn't chosen for its own sake, it's a workaround for needing runtime-composable grammars, which is the whole point of the contribution-plugin architecture. I agree it's harder to read than an EBNF file, that trade-off is inherent to supporting plugins this way rather than something specific to the CSV grammar.

@nk-coding

Copy link
Copy Markdown
Collaborator

Basically that abstract syntax is an internal TS DSL for what regular Langium projecta do via its external DSL
it's not that much different, both are transpiled to the Langium AST
for the reason why I did this: exactly as described, to allow for this runtime composition
which would have been possible with a huge hack and the regular DSL
at least for me it was not that hard to read and write after an initial adjustment phase, at least compared to the actual Langium AST json structure
but if this will ever really be an issue in the future, it should be easy to add an Langium external DSL parser via its dependency too

@szschaler

Copy link
Copy Markdown
Member

Thanks. It's probably worth splitting this out as a separate issue: #30.

I still don't understand why this forces us to have different parentheses, though. The rules for import and column mapping have different first tokens, so are clearly differentiable by an LL parser. Why does this not happen in the composed grammar?

createCsvNodes only emitted a header label, so a CSV-imported instance
rendered as a bare "Company_0 : Company" box with none of its data, while
hand-authored objects showed their property assignments. The row data was
read and then discarded.

Columns are now matched to properties by name across the class' extension
chain and rendered the same way hand-authored values are, using the same
type interpretation the import itself applies so the diagram agrees with
the imported result. Labels are read-only since edits could not be written
back to the CSV file.
The contribution-plugin refactor stripped every doc comment out of
modelTypes.ts, which was unintentional collateral rather than a deliberate
change. Restored them and added comments for the two new declarations.

Also removes infra/.DS_Store and infra/docker/.DS_Store from tracking. They
match .gitignore already, so they were committed before the ignore rule
applied to them.
The model service imported language-model-csv directly and ran the CSV
import itself, so adding any further import format meant changing the model
service, and the model service knew about a specific plugin's data format.

Follows the approach the config service already uses for its sections. The
model service now extracts the source text of each contributed import block
and forwards it to the plugin's own language service via sendPluginRequest,
then merges the returned instances and links with the hand-authored ones. The
CSV parsing and file reading move into service-model-csv behind a new
model-import request handler.

Adds service-model-common with the request and response contract, mirroring
service-config-common. Only parser errors fail the plugin-side parse, since
the plugin's service has no metamodel document to link cross-references
against; the model service has already validated those on the real document,
so plugins read names from $refText.

Also drops the hardcoded CSV entries from the model language's syntax
keywords. Contribution plugins already declare these through
additionalKeywords, which the workbench merges in.
# Conflicts:
#	platform/backend/build.gradle.kts
#	platform/common/src/main/kotlin/com/mdeo/common/model/ApiResult.kt
Brings in the import delegation refactor, so the explicit column mapping
now travels with the import block's own text to service-model-csv rather
than being read in the model service. The mapping is forwarded to
importCsvEntries there.

The diagram's CSV nodes honour the explicit mapping too, so a file whose
column names do not match the metamodel still shows its values rather than
appearing empty.
The mapping block was introduced by a "with" keyword on the grounds that
reusing braces made the grammar ambiguous. That was wrong. Tested against the
composed grammar with the keyword removed: a file mixing a mapped and an
unmapped import parses with no errors and imports correctly, and the parser
reports no ambiguity.

The block can only start with "{", while the enclosing import list continues
with an ID or ends with "}", so one token of lookahead is enough. The
comparison drawn to the model-transformation grammar was also wrong: the rule
there separates two adjacent brace blocks, which is a different situation from
a single optional block after a string.

Uses plain nested braces, matching the nested blocks in the other DSLs.
@amirrza777

Copy link
Copy Markdown
Contributor Author

You were right, and my earlier explanation was wrong. I tested it instead of reasoning about it this time.

I removed the with keyword, rebuilt the services, and parsed this against the composed grammar:

using "./test.mm"

import CSV {
    Person from "staff.csv" {
        "full_name" = name
    }
    Company from "employees.csv"
}

It parses with no errors, no ambiguity warnings from Chevrotain, and imports correctly (Person_0/Person_1 from the mapped file, Company_0/Company_1 from the unmapped one). So there is no ambiguity, and nothing forced a different delimiter.

Your reasoning was the correct one: the mapping block can only start with {, while the enclosing import list either continues with an ID or ends with }, so a single token of lookahead is enough to tell them apart.

The comparison I drew to match { pattern } then { block } was also wrong. That rule has two adjacent brace blocks, where the keyword does carry information. Here there is one optional block after a string, which is not the same situation.

I have pushed the change: the mapping now uses plain nested braces, which is also what @nk-coding originally asked for.

Person from "staff.csv" {
    "full_name" = name
}

I should not have justified a syntax change with a parser limitation I had not verified. Sorry for the detour.

Row numbering restarted at zero for each import entry, so importing the same
class from two files produced two instances called ClassName_0, two called
ClassName_1, and so on. Links refer to instances by name, so a collision would
silently attach a link to the wrong object.

Each entry is now numbered from the count of rows already imported for its
class. The diagram numbers its nodes the same way, so node names still match
the imported data.
# Conflicts:
#	app/packages/language-model/src/features/diagram-server/modelGModelFactory.ts
@szschaler
szschaler requested a review from Copilot August 3, 2026 07:39
@szschaler

Copy link
Copy Markdown
Member

Thanks for looking into this again and finding a fix.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the model CSV import to support an explicit column-to-property mapping and introduces a contribution-plugin-based architecture so the model service can delegate import parsing/execution to plugin services (CSV now implemented as a contribution).

Changes:

  • Adds CSV and model-CSV plugin services (Docker + dev compose + nginx routing) and registers them as default plugins in dev.
  • Introduces a generic “model import contribution plugin” mechanism in language-model and updates service-model to forward import blocks to contribution plugins and merge returned instances/links.
  • Implements CSV parsing/import mapping logic plus diagram rendering for CSV-backed instances, and adds a minimal standalone .csv language plugin/service.

Reviewed changes

Copilot reviewed 50 out of 52 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
infra/docker/workbench/nginx.conf Adds reverse-proxy routes for /plugin/csv/ and /plugin/model-csv/.
infra/docker/service-model-csv/Dockerfile Builds and packages the model-csv plugin service image.
infra/docker/service-csv/Dockerfile Builds and packages the csv language service image.
infra/docker-compose-dev.yaml Wires new csv/model-csv services into dev, env vars, healthchecks, and ports.
app/tsconfig.build.json Adds TS project references for new language/service packages.
app/packages/service-model/tsconfig.json Adds reference to service-model-common and reformats config.
app/packages/service-model/src/index.ts Updates model service plugin metadata and token keywords handling for contributed imports.
app/packages/service-model/src/handler/modelDataHandler.ts Delegates contributed imports to plugin services and merges imported data into ModelData.
app/packages/service-model/package.json Adds dependencies needed for delegated import handling.
app/packages/service-model-csv/vite.config.ts Builds ESM language.js static entry for the model-csv service.
app/packages/service-model-csv/tsconfig.json TS config for the model-csv service package.
app/packages/service-model-csv/src/served/language.ts Serves modelCsvPluginProvider for the language server to import.
app/packages/service-model-csv/src/index.ts Registers model-csv service + contribution plugin and request handler wiring.
app/packages/service-model-csv/src/handler/csvImportRequestHandler.ts Handles forwarded model-plugin requests, reads CSV files, returns instances/links/warnings.
app/packages/service-model-csv/package.json Declares dependencies and build scripts for model-csv service.
app/packages/service-model-common/tsconfig.json TS config for new shared model-plugin request/response types.
app/packages/service-model-common/src/modelPluginTypes.ts Defines metamodel info + model plugin request/response payload contracts.
app/packages/service-model-common/src/modelPluginHandler.ts Builds synthetic partial documents for contribution plugins to parse.
app/packages/service-model-common/src/index.ts Exports model plugin handler + types.
app/packages/service-model-common/package.json Declares new @mdeo/service-model-common package.
app/packages/service-csv/vite.config.ts Builds ESM language.js static entry for the csv service.
app/packages/service-csv/tsconfig.json TS config for the csv service package.
app/packages/service-csv/src/served/language.ts Serves csvPluginProvider for the language server to import.
app/packages/service-csv/src/index.ts Registers CSV language plugin/service for .csv files.
app/packages/service-csv/package.json Declares new @mdeo/service-csv package.
app/packages/language-shared/src/util/csv.ts Adds RFC4180-ish CSV text parser utility.
app/packages/language-shared/src/index.ts Re-exports CSV parsing utility.
app/packages/language-model/src/plugin/resolvePlugins.ts Adds contribution plugin resolution + wrapper rule generation for import <Keyword> ....
app/packages/language-model/src/plugin/modelContributionPlugin.ts Defines model contribution plugin contract (imports, grammar, deps, exports).
app/packages/language-model/src/modelPlugin.ts Merges contributed import wrapper rules into the Model language root rule at plugin creation time.
app/packages/language-model/src/index.ts Exports new contribution plugin APIs.
app/packages/language-model/src/grammar/modelTypes.ts Adds BaseModelImport and Model.imports to support contributed imports in AST.
app/packages/language-model/src/grammar/modelRules.ts Builds a dynamic Model root rule including contributed import alternatives.
app/packages/language-model/src/features/modelScopeProvider.ts Extends class reference scoping to cover class refs inside contributed imports.
app/packages/language-model/src/features/diagram-server/modelMetadataManager.ts Preserves metadata for CSV-rendered nodes when CSV import exists.
app/packages/language-model/src/features/diagram-server/modelGModelFactory.ts Renders synthetic diagram nodes from CSV row data (plus mapping support).
app/packages/language-model-csv/tsconfig.json TS config for language-model-csv.
app/packages/language-model-csv/src/plugin/modelCsvContributionPlugin.ts Defines the CSV model contribution plugin and its serialized grammar.
app/packages/language-model-csv/src/index.ts Exports language-model-csv public API.
app/packages/language-model-csv/src/grammar/csvImportTypes.ts Defines CSV import AST types (class import + mappings).
app/packages/language-model-csv/src/grammar/csvImportRules.ts Defines CSV import grammar rules (class import + mappings).
app/packages/language-model-csv/src/features/csvImport.ts Implements CSV-to-model import execution with explicit mapping + warnings.
app/packages/language-model-csv/src/csvPlugin.ts Provides standalone model-csv generated language plugin for parsing forwarded import blocks.
app/packages/language-model-csv/package.json Declares new @mdeo/language-model-csv package.
app/packages/language-csv/tsconfig.json TS config for language-csv.
app/packages/language-csv/src/index.ts Exports language-csv public API.
app/packages/language-csv/src/grammar/csvTypes.ts Defines CSV file AST type.
app/packages/language-csv/src/grammar/csvRules.ts Defines CSV grammar (captures whole file as text).
app/packages/language-csv/src/csvPlugin.ts Provides CSV language plugin provider.
app/packages/language-csv/package.json Declares new @mdeo/language-csv package.
app/package-lock.json Adds workspace links for new packages.
.gitignore Adds .DS_Store ignore entry.
Files not reviewed (1)
  • app/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/packages/language-model-csv/src/grammar/csvImportRules.ts
Comment thread app/packages/language-model-csv/src/grammar/csvImportTypes.ts
Comment thread app/packages/language-csv/src/grammar/csvRules.ts
Comment on lines +525 to +527
const uri = resolveRelativePath(doc, entry.file ?? "");
const csvContent = await this.modelState.languageServices.shared.workspace.FileSystemProvider.readFile(uri);
const rows = parseCsv(csvContent);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly right, and I want to be precise about which part, because the suggested change would break the path that actually runs.

Confirmed: langiumPool does bind EmptyFileSystemProvider for the backend services, and this read does not register a file dependency.

However ServerApi is bound only in service-common langiumPool, for the backend services. The workbench has no ServerApi at all, it supplies a real fileSystemProvider through lspFileSystem. The GLSP diagram server runs in the workbench, which is why CSV nodes do render with their values today. Switching this to ServerApi.readFile() would therefore break the environment where the diagram is actually built.

I also could not find a path where the backend builds the GModel. ModelDiagramModule is configured in postCreate, so it is registered in the services, but registration is not the same as a diagram session being served there. If that ever changes, this would silently drop every CSV node, because the catch is bare.

I have left the code as it is rather than make a speculative change, but I am happy to add a hybrid read, ServerApi when present and FileSystemProvider otherwise, if you would rather have it defensive now. Leaving this thread open for a view on that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nk-coding can you comment on this, please?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks very weird to me
why not obtain the langium document and get the content from there?
but more importantly the language-model should know nothing about CSV itself
this again needs to be handled by making the a service in the virtual model-csv language handle all this
(this is also an issue already in the earlier PR, but I could understand if you do not want to go back for this)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and your suggestion is better than what I defended. Raised as #35 with the details.

On reading the Langium document instead: that is the better answer and I should have reached for it. .csv is a registered language, so the document already exists and I can take the content from there rather than going to the file system at all. It also makes the Copilot point above moot, since it stops depending on which FileSystemProvider is bound.

On the larger point, I measured where the concept actually leaks:

package lines mentioning CSV
editor-model 0
protocol-model 0
service-model 2, both prose in one doc comment
language-model 64

So it is only language-model, and there it is real logic rather than stray references: createCsvNodes and its helpers, a CSV_ID_COLUMN constant, and the csv-node- prefix the metadata manager keys off.

The cause is that the grammar and the data computation both have a generic extension point, and the diagram never got one, so rendering reaches into the import by wrapper type name and parses the CSV itself. The fix is to give the diagram the same treatment, so a contribution plugin supplies the nodes for its own import and the CSV specifics move into language-model-csv.

The part that needs a decision is node identity: layout metadata is persisted per node id and the ids are currently csv-node-N, invented by the factory, so a generic point needs plugins to own stable ids without the host inventing a namespace per format. Written up in #35.

Happy to do it in this PR or as a follow up, whichever you prefer. Given you are ready to merge #23, my instinct is a follow up so this does not hold that up, but I am fine either way.

An empty .csv could not be parsed, because ANY_TEXT requires at least one
character and the rule assigned it unconditionally. The CSV language sets
newFileAction, so every file made with "Create New CSV" was in a parse error
state until something was typed into it. The assignment is now optional, and
the interface attribute with it. The terminal still requires a character,
since a terminal that matches the empty string would not advance the lexer.

ModelMetadataManager also passed the metadata under validation to
extractGraphMetadata through an instance field, because the base signature
only carries the source model. It is now restored in a finally block, so a
nested or concurrent validation cannot observe another document's metadata.
@nk-coding

Copy link
Copy Markdown
Collaborator

maybe to make that thing a bit clearer: the concept "CSV" should not exist in language/editor/protocol/service-model
this is always an architecture smell as the general model language should not know about the CSV support in this plugin archtecture

Running the workbench outside Docker had no route to the two new plugin
services, so they were unreachable in local dev even though nginx proxies
them in the Docker setup.

Vite matches proxy prefixes in declaration order, so model-csv has to come
before model, the same way model-transformation already does, otherwise
/plugin/model swallows it. Plain csv goes last. Added a comment recording
that, since the ordering is not obvious from the list itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants